package net.nycjava.skylight1.dependencyinjection;

import static java.lang.String.format;

import java.util.HashMap;
import java.util.Map;

/**
 * Factory that creates objects with their dependencies injected. Object dependencies are required to be coded as fields
 * (static or instance) annotated with the Dependency annotation. Supports object graph cycles through a proxy-based
 * lazy-dependency-injection of interface types; class types require eager-dependency-injection.
 * 
 * Example usage: <code>
 // register implementations
 DependencyInjectingObjectFactory dependencyInjectingObjectFactory = new DependencyInjectingObjectFactory();
 dependencyInjectingObjectFactory.registerImplementationClass(Y.class, YImpl.class);
 dependencyInjectingObjectFactory.registerImplementationClass(X.class, XImpl.class);

 // obtain an instance of Y with its dependency on X pre-populated
 Y y1 = dependencyInjectingObjectFactory.getObject(Y.class);
 * </code>
 */
public class DependencyInjectingObjectFactory {

    static final class DependencyKey {
        Class<?> clz;
        String name;

        DependencyKey(Class<?> clz, String name) {
            super();
            this.clz = clz;
            this.name = name;
        }

        public String toString() {
            if (name == "") {
                return clz.getName();
            }
            return clz.getName() + "[" + name + "]";
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((clz == null) ? 0 : clz.hashCode());
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof DependencyKey) {
                DependencyKey obj = (DependencyKey) o;
                if (clz.equals(obj.clz)) {
                    if (name == null) {
                        if (obj.name == null) {
                            return true;
                        }
                    } else {
                        if (name.equals(obj.name)) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

    }

    private Map<DependencyKey, ObjectSource<?>> implementationSources = new HashMap<DependencyKey, ObjectSource<?>>();

    /**
     * Registers a class as the implementor of the type. A <b>new</b> instance of the class will be injected into every
     * matching dependency.
     */
    public <T, S extends T> void registerImplementationClass(Class<T> anInterface, Class<S> anImplementationClass) {
        registerImplementationClass(anInterface, null, anImplementationClass);
    }

    /**
     * Registers a class as the implementor of the type. A <b>new</b> instance of the class will be injected into every
     * matching dependency.
     */
    public <T, S extends T> void registerImplementationClass(Class<T> anInterface, String name,
            Class<S> anImplementationClass) {
        DependencyKey key = new DependencyKey(anInterface, name);
        checkClassNotAlreadyRegistered(key);
        implementationSources.put(key, new ImplementationClassObjectSource<T, S>(anImplementationClass));
    }

    public <T, S extends T> void registerSingletonImplementationClass(Class<T> anInterface,
            Class<S> anImplementationClass) {
        registerSingletonImplementationClass(anInterface, null, anImplementationClass);
    }

    /**
     * Registers a class as the implementor of the type. A <b>single</b> instance of the class will be injected into
     * every matching dependency.
     */
    public <T, S extends T> void registerSingletonImplementationClass(Class<T> anInterface, String name,
            Class<S> anImplementationClass) {
        DependencyKey key = new DependencyKey(anInterface, name);
        checkClassNotAlreadyRegistered(key);
        implementationSources.put(key, new ImplementationClassSingletonObjectSource<T, S>(anImplementationClass));
    }

    /**
     * Registers a single object as the implementor of the type. The object will be injected into every matching
     * dependency.
     */
    public <T, S extends T> void registerImplementationObject(Class<T> anInterface, S anImplementationObject) {
        registerImplementationObject(anInterface, null, anImplementationObject);
    }

    /**
     * Registers a single object as the implementor of the type. The object will be injected into every matching
     * dependency.
     */
    public <T, S extends T> void registerImplementationObject(Class<T> anInterface, String name,
            S anImplementationObject) {
        DependencyKey key = new DependencyKey(anInterface, name);
        checkClassNotAlreadyRegistered(key);
        implementationSources.put(key, new SingletonObjectSource<T>(anImplementationObject));
    }

    /**
     * Registers a single object as the implementor of the type. The object will be injected into every matching
     * dependency.
     */
    public <T, S extends T> void registerConstant(Class<T> anInterface, S anImplementationObject) {
        registerImplementationObject(anInterface, null, anImplementationObject);
    }

    /**
     * Registers a single object as the implementor of the type. The object will be injected into every matching
     * dependency.
     */
    public <T, S extends T> void registerConstant(Class<T> anInterface, String name, S anImplementationObject) {
        DependencyKey key = new DependencyKey(anInterface, name);
        checkClassNotAlreadyRegistered(key);
        implementationSources.put(key, new SingletonObjectSource<T>(anImplementationObject, true));
    }

    @SuppressWarnings("unchecked")
    <T, S extends T> ObjectSource<S> getObjectSource(DependencyKey key) {
        return (ObjectSource<S>) implementationSources.get(key);
    }

    private void checkClassNotAlreadyRegistered(DependencyKey key) {
        if (implementationSources.containsKey(key)) {
            throw new IllegalArgumentException(format(
                    "Unable to register for the key %s as it is already registered: %s.", key, implementationSources
                            .get(key).toString()));
        }
    }
}
